Strom sparen mit AVRs

Die AVRs von Atmel bieten ja wirklich ein Füllhorn von Funktionen. Selbst ein winziger, 8-beiniger ATtiny13, den ich gerade in einem Projekt verbaut habe, hat dermaßen viele Funktionen, das ich vermutlich nur die Hälfte nutze. Eine Funktion, die ich aber schon immer mal ausprobieren wollte, sind die diversen Sleep-Modes zum Stromsparen. Und dieses Projekt lud gerade dazu ein: Ein elektronische Zielscheibe, die eine paar LEDs leuchten lässt und eine Melodie spielt, wenn man sie lange genug mit einem Laser in der Mitte trifft. Das ganze wird von einer Batterie angetrieben, und die soll natürlich möglichst lange halten.

Die meiste Zeit ist das Gerät im Leerlauf und wartet darauf, das es angeleuchtet wird. Dazu überprüft es alle 200ms den Spannungspegel an einem Lichtsensor. Entsprechend habe ich auch erst Mal nur versucht, den Stromverbrauch im Leerlauf zu drücken, während die LEDs blinken geht sowieso ein Großteil des Stroms für die LEDs drauf. (BTW: 6 LEDs, ein Lautsprecher und ein Lichtsensor an 5 Pins, das geht 😉 )

Ausgangslage war ein Stromverbrauch von 6mA. Schon recht ordentlich, aber da geht ja bestimmt noch was. Der erste Schritt war eine Reduzierung der Taktrate. Der ATtiny13 hat drei interne Oszillatoren: 9,6MHz, 4,5MHz und 128kHz. Ohne weitere Einstellung läuft er auf dem 9,6MHz Oszillator. Nach kurzer Überlegung kam ich dann zu dem Schluss, das eigentlich 128kHz mehr als genug sind, schließlich tut die CPU die meiste Zeit nichts. Auswählen kann man die Clock-Source über das ändern der Fuses. Bei der Gelegenheit habe ich auch gleich noch mal die Brown-Out-Detection abgeschaltet, die die CPU anhält, wenn die Spannung unter die nötigen 2,7V fällt. Das verhindert zwar, das die CPU Fehler macht, weil die Batterie ein bisschen schwächelt oder das Netzteil sich gerade abschaltet, kostet aber Strom. Und wir betreiben schließlich kein Kernkraftwerk damit. Die ganze Aktion war auf jeden Fall schon mal erfolgreich: Mit der Reduzierung der Taktrate und Abschaltung der Brown-Out-Detection war ich schon mal bei ~0,9mA, also gut ein Sechstel des Anfangswertes!

Probleme gab es kaum. Natürlich muss man F_CPU wieder richtig definieren (also auf 128000), ansonsten werden die Warteschleifen von _delay_ms() viiiiel zu lang. Außerdem hatte ich noch den Timer benutzt, um Töne auf dem Lautsprecher zu erzeugen, hier musste ich den Vorteile reduzieren und die OCRA-Werte neu berechnen. Dafür hatte ich aber sowieso ein Makro definiert, das war also kein Problem. Man kann beim Multiplexen der LEDs jetzt manchmal ein minimales Flakern sehen, aber das ist wirklich kaum wahrnehmbar.

Es sei hier vielleicht noch angemerkt, dass die ATmega’s ein Register haben, mit dem man die Taktrate zu Laufzeit ändern kann. Das ist sogar noch praktischer, denn dann kann man beim warten die CPU-Frequenz runter drehen, und wenn es was zu rechnen gibt, schnell wieder Gas geben. Man muss dann nur mit den Warteschleifen aufpassen…

Nun gut, aber da geht ja bestimmt noch mehr. Der tiny kennt drei Schlafmodi: Idle, ADC-Noise-Reduction und Power-Down. Im ersten Modus wird vor allem die Clock der CPU angehalten, aber der Rest läuft weiter. So kann man stromsparend auf Interrupts warten. Im zweiten Modus wird ein bisschen mehr abgeschaltet, hier geht es vor allem darum während einer Analog-Digital-Wandlung möglichst wenig Rauschen im System zu haben. Und schlussendlich wird im dritten Modus praktisch alles abgeschaltet. Hier kann nur noch ein externer(!) Interrupt die CPU wecken oder der zubeißende Watchdog (der einen eigenen Oszillator hat, der immer läuft).

Als nächstes habe ich also versucht, das Polling beim Warten auf das Ende der ADC-Wandlung durch schlafen im Noise-Reduction-Modus zu ersetzen. Hier ein bisschen Code:


ADCSRA |= (_BV(ADEN) | _BV(ADIE) |_BV(ADPS2) | _BV(ADPS0)); //turn ADC on, enable interrupt,  ADC clock prescaler = 32

sei(); //we need interrupts for the ADC wakeup

uint8_t readAnalog()
{
//ADCSRA |=_BV(ADSC);
//loop_until_bit_is_clear(ADCSRA, ADSC);
set_sleep_mode(SLEEP_MODE_ADC);
sleep_mode();
//will wakeup here
return ADCH;
}

//this interrupt will be called after wakeup from ADC sleep,
//and therefore must be defined...
EMPTY_INTERRUPT(ADC_vect);

Wichtig ist hier zum einen der ADC-Interrupt: Er muss aktiviert sein (ADIE Bit in ADCSRA setzen), die globalen Interrupts müssen an sein (sei()), und – ganz wichtig – es muss auch ein Handler definiert sein! Ohne den Handler steht in der Interrupt-Tabelle ein rjump __bad_vector und die CPU hängt. Die C Bibliothek definiert praktischerweise ein Makro EMPTY_INTERRUPT genau für diesen Zweck. (In die Tabelle wir dann direkt ein reti geschrieben).

In der C-Bibliothek gibt es in der Datei sleep.h auch gleich ein paar Funktionen um die Schlafmodi zu steuern, wie man oben sieht. Wichtig ist noch, das beim Übergang in der ADC Schlafmodus automatisch eine Umwandlung gestartet wird, man muss also nicht mal vorher ADSC setzen. Wenn die CPU nicht mehr aufwacht unbedingt noch mal prüfen ob der Interrupt wirklich aktiv ist und ausgeführt wird. Ohne Interrupt bekommt die CPU nicht mit, wenn die Umwandlung zu Ende ist.

Und was hat es nun gebracht? Leider nicht so wirklich viel, zumindest was den Stromverbrauch angeht. Vielleicht hat sich ja die Genauigkeit ein bisschen erhöht…

Ein klein bisschen Strom kann man auch noch sparen, in dem man die Module der CPU abschaltet, die man nicht braucht. In meinem Fall sind das der Analoge Komperator und der GPIO Block des Pins an dem der Lichtsensor hängt (wenn der aktiv ist, kann man den Pin gleichzeitig auch noch digital auslesen, was jetzt vielleicht nicht so viel Sinn ergibt…) Bei größeren Modellen gibt es natürlich noch viel mehr Sachen, die man abschalten kann, und es gibt auch Funktionen in der C-Bibliothek dafür.

Ich habe das mit diesem Code erledigt:


DIDR0 = _BV(ADC3D); //disable ADC3 (PB3) as digital input

ACSR = _BV(ACD); //disable the analog comperator

Das hat noch mal ca. 100µA gebracht.

(Weiter auf der nächsten Seite, leider etwas schlecht zu sehen…)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert