![]() |
Arduino schlafen legenProf. Jürgen Plate |
Bei batteriegestützten Anwendungen ist die Leistungsaufnahme ein wichtiges Kriterium. Aber auch bei Anwendungen mit ausreichender Stromversorgung kann es sinvoll sein, den Arduino zwischendurch schlafen zu legen. Wenn beispielsweise nur alle paar Minuten ein Messwert erfaßt wird, kann bei Dauerbetrieb ein sinnvoller Einsatz der Stromsparmöglichkeiten des Mikrocontrollers durchaus Geld sparen. Im Normalmodus (auch wenn delay() verwendet wird, liegt der Stromverbrauch bei ca. 50 bis 60 mA. Im Schlafmodus geht er auf weniger als 40 mA zurück. Das ist immer noch relativ viel, was aber an den zusätzlichen Elementen auf der Uno-Platine liegt. Alleine die Power-LED nimmt knapp 10 mA auf und auch die USB-Schnittstelle braucht Einiges.
Schon bei der Hardware gibt also es Optimierungsmöglichkeiten. Dazu gehören unter anderem Onboard-Spannungsregler, Schnittstellen-Bausteine, die obligatorischen LEDs etc. Befindet sich ein Board im Dauerbetrieb, ist eine ständig mit Strom versorgte FTDI-Schnittstelle ebenso ein Energiefresser wie die Power-LED. In solchen Fällen empfiehlt sich der Einsatz eines möglichst rudimentären und auf den Zweck zugeschnittenen Boards, welches auf überflüssige Komponenten verzichtet, z. B. eine Fassung für den Controller mit dem Quarz und den nötigen passiven Bauelementen auf einer Lochrasterplatte. Den Controller programmiert und testet man auf dem Arduino-Board und steckt ihn dann auf die Lochrasterplatte (der Arduino bekommt einen neuen Controller, auf den dann nur der Bootloader geladen werden muss).
Idle Mode (SLEEP_MODE_IDLE): Wenn keine Berechnungen erfolgen sollen, Timer, UART, ADC etc. aber aktiv bleiben sollen, ist dieser Modus geeignet. CPU und Flash stoppen, der Stromverbrauch sinkt auf ca. 0,35 mA. Aus diesem Modus kann jeder Interrupt die CPU wieder wecken. Sie ist nach sechs Takten wieder voll einsatzfähig.
ADC Noise Reduction Mode (SLEEP_MODE_ADC): Hier wird zusätzlich zum Idle Mode der Takt für die IO-Komponenten abgeschaltet. Nur noch der AD-Wandler, die externen Interrupts, das TWI und der Watchdog sind funktionsfähig. Der UART, Timer0 und Timer1 sind nicht mehr nutzbar. Dies dient auch dazu, die Messung des ADC geringfügig zu verbessern.
Power Save Mode (SLEEP_MODE_PWR_SAVE): Hier werden fast alle Oszillatoren mit Ausnahme von Timer2 gestoppt. Dieser Timer könnte bei Bedarf mit einem externen 32,768-kHz-Uhrenquarz betrieben werden. Ist er entsprechend konfiguriert, dann bleibt er beim Einschalten des Power Save Mode aktiv.
Stand By (SLEEP_MODE_STANDBY): In diesem Modus werden alle Takte des Controllers gestoppt, nur der Hauptoszillator läuft weiter. Das kostet zwar etwas Energie, aber dadurch ist die CPU nach dem Aufwachen nach nur sechs Takten wieder bereit. Weil alle Oszillatoren gestoppt sind, funktionieren nur noch die asynchronen Komponenten.
Power Down Mode (SLEEP_MODE_PWR_DOWN): Das ist sozusagen der Tiefschlaf. Es werden alle Oszillatoren gestoppt (außer dem Watchdog-Takt, falls aktiviert). Nur asynchrone Komponenten funktionieren jetzt noch und können den schlafenden Controller wecken.
Wenn der Mikrocontroller per Programm in den Schlafmodus versetzt wird, pausiert die Ausführung des Codes an dieser Stelle. Um die Ausführung des Codes fortzusetzen, muss der Mikrocontroller wieder aus dem Schlafmodus geweckt werden, z. B. durch einen Timer, einen externen Interrupt, etc. Es gibt mehrere Arduino-Bibliotheksfunktionen, die zur Steuerung des Schlafmodus verwendet werden:
Funktion | Beschreibung |
---|---|
set_sleep_mode(Modus) | Konfiguriert den Atmega für den angegebenen Schlafmodus |
sleep_enable() | Ermöglicht die Freigabe des Schlafmodus |
sleep_mode() | Eintritt in den Schlafmodus. Bevor diese Funktion aufgerufen wird, |
sleep_disable() | Deaktiviert den Schlafmodus |
Der folgende Programmcode versetzt den Arduino in einen der o. a. Schlafmodi:
void enter_sleep(void) { set_sleep_mode(SLEEP_MODE); sleep_enable(); sleep_mode(); /** Das Programm läuft ab hier nach dem Aufwachen weiter. **/ /** Es wird immer zuerst der Schlafmodus disabled. **/ sleep_disable(); }Für SLEEP_MODE setzt man einen der Modi ein (Konstanten: SLEEP_MODE_IDLE, SLEEP_MODE_ADC, SLEEP_MODE_PWR_SAVE, SLEEP_MODE_STANDBY, SLEEP_MODE_PWR_DOWN).
Für das Aufwecken des Controllera aus dem Tiefschlaf gibt es mehrere Möglichkeiten, die auch vom jeweiligen Schlafmodus abhängen. Im Folgenden erfolgt eine Beschränkung auf den wirksamsten Schlafmodus, den Power-Down-Mode, und auf dessen Möglichkeiten zum Aufwecken:
In diesem Abschnitt wird gezeigt, wie man den Arduino über einen externen Interrupt weckt. Dazu muss mindestens eine externe Interrupt-Schaltung vorhanden sein. Selbst der Atmega168 auf dem Arduino Diecimila (einer der ersten Arduinos) hat zwei externe Interrupts: INT0 und INT1, die an den den digitalen Pins 2 und 3 angeschlossen sind. Diese Interrupts unterstützen vier Trigger-Modi für externe Interrupts:
Modus | Beschreibung |
---|---|
LOW | ein Low-Pegel |
CHANGE | eine Änderung des Pegels |
RISING | eine steigende Flanke am Trigger_Eingang |
FALLING | eine fallende Flanke am Trigger-Eingang |
Achten Sie auch darauf, dass Ihr Hardware-Setup für den Trigger-Eingang korrekt ist.
#include <avr/sleep.h> #define INT_PIN 2 #define LED_PIN 13 void INT_PINisr(void) /* ISR fuer Pin 2 */ { /* detach Interrupt, damit er nur einmal auftritt */ detachInterrupt(0); } void enter_sleep(void) { attachInterrupt(0, INT_PINisr, LOW); /* Arduino schlafen legen */ set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); sleep_mode(); sleep_disable(); } void setup() { Serial.begin(9600); Serial.println("Starte ..."); /* Pin 2 als Interrupt-Pin, Pullup einschalten */ pinMode(INT_PIN, INPUT); digitalWrite(INT_PIN, HIGH); pinMode(LED_PIN, OUTPUT); Serial.println("Init erfolgt ..."); delay(100); } void loop() { /* warten, bis der Interrupt-Eingang wieder frei ist */ while (digitalRead(INT_PIN) == LOW) { Serial.println("*** Taste loslassen ***"); delay(500); } Serial.println("Gehe schlafen ..."); delay(100); /* LED umschalten und wieder schlafen */ digitalWrite(LED_PIN, ! digitalRead(LED_PIN)); enter_sleep(); Serial.println("Bin aufgewacht ..."); delay(100); }
Nun wird der Arduino über den USB-UART (serielle Schnittstelle über USB) geweckt. Solange keine Kommunikation vom PC über die serielle Verbindung erfolgt, wird der Controller schlafen gelegt. Sobald serielle Daten vom PC an den Arduino gesendet werden, wacht er auf und ist für einen kurzen Zeitraum aktiv bevor er wieder im den Schlafmodus wechselt, wenn keine Daten vom PC Mehr kommen.
Um den Controller zu wecken, verwenden wird der externe Interrupt INT0 auf dem digitalen Pin 2 verwendet (wie oben). Diesmal ist der Pin 2 über einen 470-Ω-Widerstand mit dem digitalen Pin 0 (UART-Empfangsleitung) verbunden. Man könnte auch einen weniger effizienten Schlafmodus (SLEEP_MODE_IDLE) verwenden und alternativ den Arduino mit dem UART-Interrupt wecken.
Das Programm zum Wecken über den UART ist wegen der Vebindung zwischen Pin 2 und Pin 0 das gleiche wie oben.
Der ATmega328 verfügt über drei interne Timer:
Die Zeit lässt sich mittels Clock-Select noch maximal um den Faktor 1024 verlängern. Bei Timer0 und Timer2 ergibt sich eine maximale Zeit bis zum Überlauf von ca. 16 ms. Immer noch relativ wenig. Bei Timer1 sieht dies bei maximalem Clock-Select von 1024 etwas besser aus, da hier der Timer ein 16-Bit-Zähler ist. Hier betragt die Maximalzeit (216 * 1024)/16000000 ~ 4,2 s. Für einen lngeren Schlaf kommt daher meist nur der Timer1 in Frage. Wie schon erwähnt, sind nicht alle Komponenten des Controllers in allen Leistungsmodi verfügbar. Der niedrigste Power-Mode von Timer1 ist IDLE. In einem tieferen Schlafmodus ist der Timer abgeschaltet. Mit Timer1 kann der Arduino also ca. 4,2 s schlafen gelegt werden. Eine längere Schlafperiode kann nur über den Watchdog-Timer erreicht werden (siehe unten). Dieser kann auch in einem niedrigeren Sleep-Mode als Timer1 betrieben werden.
Für die Einstellung der Clock-Select-Bits CS10, CS11 und CS12 im Register TCCR1B bestehen folgende Möglichkeiten:
CS12 | CS11 | CS10 | Beschreibung |
---|---|---|---|
0 | 0 | 0 | kein Takt, Timer stoppt |
0 | 0 | 1 | CLKI/O/1 |
0 | 1 | 0 | CLKI/O/8 (Prescaler) |
0 | 1 | 1 | CLKI/O/64 (Prescaler) |
1 | 0 | 0 | CLKI/O/256 (Prescaler) |
1 | 0 | 1 | CLKI/O/1024 (Prescaler) |
1 | 1 | 0 | externer Takt an T1, fallende Flanke |
1 | 1 | 1 | externer Takt an T1, steigende Flanke |
Das folgende Beispiel soll folgendermaßen funktionieren:
#include <avr/sleep.h> #include <avr/power.h> #define LED_PIN 13 volatile int toggle = 0; ISR(TIMER1_OVF_vect) /* Timer1 Interrupt service Routine */ { if(toggle == 0) toggle = 1; } void enter_sleep(void) /* Arduino schlafen legen */ { set_sleep_mode(SLEEP_MODE_IDLE); sleep_enable(); /* Weil diesmal nicht der tiefste Schlafmodus gewaehlt wurde, * koennen unbenutzte Komponenten noch zusaetzlich abgeschaltet * werden, um Energie zu sparen. Noch wichtiger: Die Komponenten * koennten ggf. den Arduino ungewollt aufwecken. */ power_adc_disable(); /* Analog-Eingaenge abschalten */ power_spi_disable(); /* SPI abschalten */ power_timer0_disable(); /* Timer0 abschalten */ power_timer2_disable(); /* Timer0 abschalten */ power_twi_disable(); /* TWI abschalten */ sleep_mode(); sleep_disable(); power_all_enable(); /* Komponenten wieder aktivieren */ } void setup() { Serial.begin(9600); Serial.println("Starte ..."); delay(100); pinMode(LED_PIN, OUTPUT); /* Timer konfigurieren */ TCCR1A = 0x00; /* Normalbetrieb /* TCNT1 = 0x0000; /* Zaehler loeschen */ TCCR1B = 0x05; /* Prescaler: 1024 */ TIMSK1 = 0x01; /* Timer-Interrupt einschalten */ Serial.println("Init erfolgt ..."); delay(100); } void loop() { if(toggle) { toggle = 0; /* LED umschalten und wieder schlafen */ digitalWrite(LED_PIN, ! digitalRead(LED_PIN)); enter_sleep(); } }
Eine Möglichkeit, einen etwas längere Timer zu nutzen, ist das Aufwecken durch den Watchdog, dieser ist in der Lage den Controller nach 8 s wieder zu wecken. Der Watchdog-Timer (WDT) auf dem Arduino-Mikroprozessor hat einen eigenen internen 128-kHz-Oszillator (im Gegensatz zu den 8/16-Bit-Timern, die entweder den 16-MHz-Systemtakt oder einen externen Takt verwenden). Es ist dieser separate Oszillator, der es dem Watchdog ermöglicht, im niedrigsten Leistungsmodus SLEEP_MODE_PWR_DOWN zu arbeiten. Für die folgenden Einstellungen des Watchdog spielt das Watchdog Timer Controller Register eine entscheidende Rolle:
Der WDT verfügt auch über einen Prescaler, mit dem die Timeout-Periode konfiguriert wird. Es unterstützt Zeiten von 16 ms bis 8 Sekunden, die über die Bits WPD0 bis WPD3 eingestellt werden.
Der Watchdog hat drei Betriebsarten:
WDTON | WDE | WDIE | Modus | Aktion bei Timeout |
---|---|---|---|---|
1 | 0 | 0 | gestoppt | keine |
1 | 0 | 1 | Interrupt-Modus | Interrupt |
1 | 1 | 0 | Systemreset-Modus | Reset |
1 | 1 | 1 | Interrupt- und Systemreset-Modus | Interrupt, anschlie?end Reset |
0 | x | x | Systemreset-Modus | Reset |
In diesem Beispiel wird alle acht Sekunden ein WDT-Interrupt ausgelöst, der die LED umschaltet und dann den Controller wieder schlafen legt. Auch hier muss die globale Variable wieder als volatile deklariert werden.
#include <avr/sleep.h> #include <avr/power.h> #include <avr/wdt.h> #define LED_PIN (13) volatile int toggle = 1; ISR(WDT_vect) /* Watchdog imer Interrupt Service Routine */ { if(toggle == 0) { toggle = 1; } else { Serial.println("WDT Overrun Error!"); } } void enter_sleep(void) /* Arduino schlafen legen */ { set_sleep_mode(SLEEP_MODE_PWR_SAVE); /* Es geht auch SLEEP_MODE_PWR_DOWN */ sleep_enable(); power_adc_disable(); /* Analog-Eingaenge abschalten */ power_spi_disable(); /* SPI abschalten */ power_timer0_disable(); /* Timer0 abschalten */ power_timer2_disable(); /* Timer0 abschalten */ power_twi_disable(); /* TWI abschalten */ sleep_mode(); sleep_disable(); power_all_enable(); /* Komponenten wieder aktivieren */ } void setup() { Serial.begin(9600); Serial.println("Starte ..."); delay(100); pinMode(LED_PIN,OUTPUT); /* Setup des Watchdog Timers */ MCUSR &= ~(1<<WDRF); /* WDT reset flag loeschen */ WDTCSR |= (1<<WDCE) | (1<<WDE); /* WDCE setzen, Zugriff auf Presclaler etc. */ WDTCSR = 1<<WDP0 | 1<<WDP3; /* Prescaler auf 8.0 s */ WDTCSR |= 1<<WDIE; /* WDT Interrupt freigeben */ Serial.println("Init erfolgt ..."); delay(100); } void loop() { if(toggle) { /* LED umschalten und wieder schlafen */ toggle = 0; digitalWrite(LED_PIN, !digitalRead(LED_PIN)); enter_sleep(); } }